<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes';
import { isClient, useEventListener, useWindowSize } from '@vueuse/core';
import type { CSSProperties } from 'vue';

defineOptions({ name: 'Sticky' });

const props = defineProps({
  // 距离顶部或者底部的距离(单位px)
  offset: propTypes.number.def(0),
  // 设置元素的堆叠顺序
  zIndex: propTypes.number.def(999),
  // 设置指定的class
  className: propTypes.string.def(''),
  // 定位方式，默认为(top)，表示距离顶部位置，可以设置为top或者bottom
  position: {
    type: String,
    validator: function (value: string) {
      return ['top', 'bottom'].indexOf(value) !== -1;
    },
    default: 'top'
  }
});
const width = ref('auto' as string);
const height = ref('auto' as string);
const isSticky = ref(false);
const refSticky = shallowRef<HTMLElement>();
const scrollContainer = shallowRef<HTMLElement | Window>();
const { height: windowHeight } = useWindowSize();
onMounted(() => {
  height.value = refSticky.value?.getBoundingClientRect().height + 'px';

  scrollContainer.value = getScrollContainer(refSticky.value!, true);
  useEventListener(scrollContainer, 'scroll', handleScroll);
  useEventListener('resize', handleResize);
  handleScroll();
});
onActivated(() => {
  handleScroll();
});

const camelize = (str: string): string => {
  return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ''));
};

const getStyle = (element: HTMLElement, styleName: keyof CSSProperties): string => {
  if (!isClient || !element || !styleName) return '';

  let key = camelize(styleName);
  if (key === 'float') key = 'cssFloat';
  try {
    const style = element.style[styleName];
    if (style) return style;
    const computed = document.defaultView?.getComputedStyle(element, '');
    return computed ? computed[styleName] : '';
  } catch {
    return element.style[styleName];
  }
};
const isScroll = (el: HTMLElement, isVertical?: boolean): boolean => {
  if (!isClient) return false;
  const key = (
    {
      undefined: 'overflow',
      true: 'overflow-y',
      false: 'overflow-x'
    } as const
  )[String(isVertical)]!;
  const overflow = getStyle(el, key);
  return ['scroll', 'auto', 'overlay'].some((s) => overflow.includes(s));
};

const getScrollContainer = (
  el: HTMLElement,
  isVertical: boolean
): Window | HTMLElement | undefined => {
  if (!isClient) return;
  let parent = el;
  while (parent) {
    if ([window, document, document.documentElement].includes(parent)) return window;
    if (isScroll(parent, isVertical)) return parent;
    parent = parent.parentNode as HTMLElement;
  }
  return parent;
};

const handleScroll = () => {
  width.value = refSticky.value!.getBoundingClientRect().width! + 'px';
  if (props.position === 'top') {
    const offsetTop = refSticky.value?.getBoundingClientRect().top;
    if (offsetTop !== undefined && offsetTop < props.offset) {
      sticky();
      return;
    }
    reset();
  } else {
    const offsetBottom = refSticky.value?.getBoundingClientRect().bottom;

    if (offsetBottom !== undefined && offsetBottom > windowHeight.value - props.offset) {
      sticky();
      return;
    }
    reset();
  }
};
const handleResize = () => {
  if (isSticky.value && refSticky.value) {
    width.value = refSticky.value.getBoundingClientRect().width + 'px';
  }
};
const sticky = () => {
  if (isSticky.value) {
    return;
  }
  isSticky.value = true;
};
const reset = () => {
  if (!isSticky.value) {
    return;
  }
  width.value = 'auto';
  isSticky.value = false;
};
</script>
<template>
  <div ref="refSticky" :style="{ height: height, zIndex: zIndex }">
    <div
      :class="className"
      :style="{
        top: position === 'top' ? offset + 'px' : '',
        bottom: position !== 'top' ? offset + 'px' : '',
        zIndex: zIndex,
        position: isSticky ? 'fixed' : 'static',
        width: width,
        height: height
      }"
    >
      <slot>
        <div>sticky</div>
      </slot>
    </div>
  </div>
</template>
